package org.ddevil.data;

import java.io.Serializable;
import java.util.Hashtable;

/**
 * This extension of {@link Hashtable} provides the ability to use strings as
 * hashtable keys in a case insensitive manner.
 * 
 * @author Eric Lundin
 */
@SuppressWarnings("serial")
public class CaseInsensitiveHashtable<V> extends Hashtable<CIHTableString, V> {

	/**
	 * Create a default CaseInsensitiveHashtable with an initial capacity of 11
	 * and a load factor of .75.
	 */
	public CaseInsensitiveHashtable() {
		super();
	}

	/**
	 * Create a a CaseInsensitiveHashtable with initial room for the specified
	 * number of elements using the given load factor.
	 * 
	 * @param initialCapacity
	 *            The initial number of elements to provide space for.
	 * @param loadFactor
	 *            The load factor to use. Default is .75.
	 */
	public CaseInsensitiveHashtable(int initialCapacity, float loadFactor) {
		super(initialCapacity, loadFactor);
	}

	/**
	 * Create a CaseInsensitiveHashtable with initial room for the specified
	 * number of elements.
	 * 
	 * @param initialCapacity
	 *            The initial number of elements to provide space for.
	 */
	public CaseInsensitiveHashtable(int initialCapacity) {
		super(initialCapacity);
	}

	/**
	 * Create, or replace, a mappining from the given key to the given value.
	 * The key is case insensitive.
	 * 
	 * @param key
	 *            The key to map the value to.
	 * @param value
	 *            The value to map tot he key.
	 * @return The old object which was mapped to this key.
	 */
	public synchronized Object put(String key, V value) {
		return super.put(new CIHTableString(key), value);
	}

	/**
	 * Get the value mapped to the given key. Returns null if there is no entry
	 * for the given key. The key is case insensitive.
	 * 
	 * @param key
	 *            The key to get the mapped value for.
	 * @return The mapped value or null if there is not an entry for the given
	 *         key.
	 */
	public V get(String key) {
		return super.get(new CIHTableString(key));
	}

	/**
	 * Determine if this hashtable holds an entry for the given string. Case
	 * insensitive.
	 * 
	 * @param key
	 *            The key string to look for.
	 * @return True if there is an entry for this string. False otherwise.
	 */
	public boolean containsKey(String key) {
		return super.containsKey(new CIHTableString(key));
	}
    
    /**
     * Removes the key (and its corresponding value) from this 
     * hashtable. This method does nothing if the key is not in the hashtable.
     * Case insensitive.
     *
     * @param   key   the key that needs to be removed.
     * @return  the value to which the key had been mapped in this hashtable,
     *          or <code>null</code> if the key did not have a mapping.
     * @throws  NullPointerException  if the key is <code>null</code>.
     */
    public synchronized V remove(String key) {
        return super.remove(new CIHTableString(key));
    }

}

/**
 * This class generates case insensitive hashcodes for strings. That is the hash
 * code for "EXAMPLE" is gaurunteed to be the same as the hash code for
 * "example", "EXaMpLe", and any other case combinations of the same ordered set
 * of characters.
 * 
 * @author Eric Lundin
 */
final class CIHTableString implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
	/** The value of this object */
	private String value;

	/** Cached lowercase mapping */
	private static final char[] lowerCaseMap = new char[256];

	/** Holds the hashcode for this string */
	private int hashCode;

	// Cache the lowercase mapping.
	static {
		for (char currentChar = 0; currentChar < 256; currentChar++)
			lowerCaseMap[currentChar] = Character.toLowerCase(currentChar);
	}

	/**
	 * Create a CIHTableString with the given value.
	 * 
	 * @param value
	 *            The value to store in this object.
	 */
	public CIHTableString(String v) {
		value = v;
		hashCode = computeHash();
	}

	/**
	 * Overriden to return a hashcode which will be case insensitive. Uses
	 * normal string hashcode formula but converts to lowercase.
	 * 
	 * @return The case insensitive hashcode of this string.
	 */
	@Override
    public int hashCode() {
		return hashCode;
	}

	private int computeHash() {
		int hash = 0;
		int len = value.length();
		for (int i = 0; i < len; i++)
			hash = 31 * hash + lowerCaseMap[value.charAt(i)];
		return hash;
	}

	/**
	 * Determine if this object is considered equal to the given object.
	 * 
	 * @return True if the given object is assignable
	 */
	@Override
	public boolean equals(Object o) {
		boolean isEqual = false;
		if (o != null) {
			if (o.getClass().isAssignableFrom(getClass())) {
				isEqual = value.equalsIgnoreCase(o.toString());
			}
		}
		return isEqual;
	}

	/**
	 * Get the string value represented by this object.
	 * 
	 * @return The string represented by this object.
	 */
	public String getValue() {
		return value;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return value;
	}
}
